<script setup lang="ts">
import {ref, reactive, computed, watch, onMounted, onUnmounted} from 'vue'
import request from '@/util/request'
import language from './utils/language'
import mimes from './utils/mimes'
import data2blob from './utils/data2blob'
import effectRipple from './utils/effectRipple'
import {getCookie} from "@/util/cookieUtil"
import {cookieToken} from "@/config/CookieConfig";
import {message} from "ant-design-vue";

const props = defineProps({
  // 域，上传文件name，触发事件会带上（如果一个页面多个图片上传控件，可以做区分
  field: {
    type: String,
    default: 'avatar'
  },
  // 原名key，类似于id，触发事件会带上（如果一个页面多个图片上传控件，可以做区分
  ki: {
    type: Number,
    default: 0
  },
  // 显示该控件与否
  value: {
    type: Boolean,
    default: true
  },
  // 上传地址
  url: {
    type: String,
    default: ''
  },
  // 其他要上传文件附带的数据，对象格式
  params: {
    type: Object,
    default: null
  },
  // Add custom headers
  headers: {
    type: Object,
    default: null
  },
  // 剪裁图片的宽
  width: {
    type: Number,
    default: 200
  },
  // 剪裁图片的高
  height: {
    type: Number,
    default: 200
  },
  // 不显示旋转功能
  noRotate: {
    type: Boolean,
    default: true
  },
  // 不预览圆形图片
  noCircle: {
    type: Boolean,
    default: false
  },
  // 不预览方形图片
  noSquare: {
    type: Boolean,
    default: false
  },
  // 单文件大小限制
  maxSize: {
    type: Number,
    default: 10240
  },
  // 语言类型
  langType: {
    type: String,
    default: 'zh'
  },
  // 语言包
  langExt: {
    type: Object,
    default: null
  },
  // 图片上传格式
  imgFormat: {
    type: String,
    default: 'png'
  },
  // 是否支持跨域
  withCredentials: {
    type: Boolean,
    default: false
  }
})
const emit = defineEmits(['input', 'close', 'crop-success', 'crop-upload-success', 'crop-upload-fail'])
const fileinput = ref<HTMLInputElement | null>(null);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const allowImgFormat = ['jpg', 'png']
const tempImgFormat = allowImgFormat.indexOf(props.imgFormat) === -1 ? 'jpg' : props.imgFormat

const state = reactive({
  ...props,
  mime: mimes[tempImgFormat],
  lang: language[props.langType] ? language[props.langType] : language['en'],
  isSupported: true,
  isSupportTouch: document.hasOwnProperty('ontouchstart'),
  step: 1,
  loading: 0,
  progress: 0,
  hasError: false,
  errorMsg: '',
  ratio: props.width / props.height,
  sourceImg: null as HTMLImageElement | null,
  sourceImgUrl: '',
  createImgUrl: '',
  sourceImgMouseDown: {
    on: false,
    mX: 0,
    mY: 0,
    x: 0,
    y: 0
  },
  previewContainer: {
    width: 100,
    height: 100
  },
  sourceImgContainer: {
    width: 240,
    height: 184
  },
  scale: {
    zoomAddOn: false,
    zoomSubOn: false,
    range: 1,
    rotateLeft: false,
    rotateRight: false,
    degree: 0,
    x: 0,
    y: 0,
    width: 0,
    height: 0,
    maxWidth: 0,
    maxHeight: 0,
    minWidth: 0,
    minHeight: 0,
    naturalWidth: 0,
    naturalHeight: 0
  },
})
const progressStyle = computed(() => ({
  width: `${state.progress}%`
}))
const sourceImgMasking = computed(() => {
  const {width, height, ratio, sourceImgContainer} = state;
  const sic = sourceImgContainer;
  const sicRatio = sic.width / sic.height;
  let x = 0;
  let y = 0;
  let w = sic.width;
  let h = sic.height;
  let scale = 1;
  if (ratio < sicRatio) {
    scale = sic.height / height;
    w = sic.height * ratio;
    x = (sic.width - w) / 2;
  }
  if (ratio > sicRatio) {
    scale = sic.width / width;
    h = sic.width / ratio;
    y = (sic.height - h) / 2;
  }
  return {
    scale,
    x,
    y,
    width: w,
    height: h
  };
})
const sourceImgStyle = computed(() => {
  const {scale} = state;
  const top = `${scale.y + sourceImgMasking.value.y}px`;
  const left = `${scale.x + sourceImgMasking.value.x}px`;
  return {
    top,
    left,
    width: `${scale.width}px`,
    height: `${scale.height}px`,
    transform: `rotate(${scale.degree}deg)`
  }
})

const sourceImgShadeStyle = computed(() => {
  const {sourceImgContainer} = state;
  const sic = sourceImgContainer;
  const sim = sourceImgMasking.value;
  const w = sim.width === sic.width ? sim.width : (sic.width - sim.width) / 2;
  const h = sim.height === sic.height ? sim.height : (sic.height - sim.height) / 2;
  return {
    width: `${w}px`,
    height: `${h}px`
  };
})
const previewStyle = computed(() => {
  const {ratio, previewContainer} = state;
  const pc = previewContainer;
  let w = pc.width;
  let h = pc.height;
  const pcRatio = w / h;
  if (ratio < pcRatio) {
    w = pc.height * ratio;
  }
  if (ratio > pcRatio) {
    h = pc.width / ratio;
  }
  return {
    width: `${w}px`,
    height: `${h}px`
  }
})
watch(() => props.value, (newValue) => {
  if (newValue && state.loading !== 1) {
    reset();
  }
})
onMounted(() => {
  document.addEventListener('keyup', closeHandler);
})

onUnmounted(() => {
  document.removeEventListener('keyup', closeHandler);
})

function ripple(e: MouseEvent) {
  effectRipple(e)
}

function off() {
  setTimeout(() => {
    emit('input', false);
    emit('close');
    if (state.step === 3 && state.loading === 2) {
      setStep(1);
    }
  }, 200);
}

function setStep(no: number) {
  setTimeout(() => {
    state.step = no;
  }, 200);
}

function preventDefault(e: Event) {
  e.preventDefault();
  return false;
}

function handleClick(e: MouseEvent) {
  if (state.loading !== 1) {
    if (e.target !== fileinput.value) {
      e.preventDefault();
      if (document.activeElement !== fileinput.value) {
        fileinput.value?.click();
      }
    }
  }
}

function handleChange(e: Event) {
  e.preventDefault();
  if (state.loading !== 1) {
    const files = (e.target as HTMLInputElement).files || (e as DragEvent).dataTransfer?.files;
    reset();
    if (checkFile(files![0])) {
      setSourceImg(files![0]);
    }
  }
}

function checkFile(file: File): boolean {
  const {lang, maxSize} = state;
  if (file.type.indexOf('image') === -1) {
    state.hasError = true;
    state.errorMsg = lang.error.onlyImg;
    return false;
  }
  if (file.size / 1024 > maxSize) {
    state.hasError = true;
    state.errorMsg = `${lang.error.outOfSize}${maxSize}kb`;
    return false;
  }
  return true;
}

function reset() {
  state.loading = 0;
  state.hasError = false;
  state.errorMsg = '';
  state.progress = 0;
}

function setSourceImg(file: File) {
  const fr = new FileReader();
  fr.onload = (e) => {
    state.sourceImgUrl = e.target?.result as string;
    startCrop();
  };
  fr.readAsDataURL(file);
}

function startCrop() {
  const {
    width,
    height,
    ratio,
    scale,
    sourceImgUrl,
    lang
  } = state;
  const sim = sourceImgMasking.value;
  const img = new Image();
  img.src = sourceImgUrl;
  img.onload = () => {
    const nWidth = img.naturalWidth;
    const nHeight = img.naturalHeight;
    const nRatio = nWidth / nHeight;
    let w = sim.width;
    let h = sim.height;
    let x = 0;
    let y = 0;
    if (nWidth < width || nHeight < height) {
      state.hasError = true;
      state.errorMsg = `${lang.error.lowestPx}${width}*${height}`;
      return false;
    }
    if (ratio > nRatio) {
      h = w / nRatio;
      y = (sim.height - h) / 2;
    }
    if (ratio < nRatio) {
      w = h * nRatio;
      x = (sim.width - w) / 2;
    }
    scale.range = 0;
    scale.x = x;
    scale.y = y;
    scale.width = w;
    scale.height = h;
    scale.degree = 0;
    scale.minWidth = w;
    scale.minHeight = h;
    scale.maxWidth = nWidth * sim.scale;
    scale.maxHeight = nHeight * sim.scale;
    scale.naturalWidth = nWidth;
    scale.naturalHeight = nHeight;
    state.sourceImg = img;
    createImg();
    setStep(2);
  };
}

function imgStartMove(e: any) {
  e.preventDefault();
  if (state.isSupportTouch && !(e as TouchEvent).targetTouches) {
    return false;
  }
  const et = e.targetTouches ? e.targetTouches[0] : e;
  const {sourceImgMouseDown, scale} = state;
  const simd = sourceImgMouseDown;
  simd.mX = et.screenX;
  simd.mY = et.screenY;
  simd.x = scale.x;
  simd.y = scale.y;
  simd.on = true;
}

function imgMove(e: any) {
  e.preventDefault();
  if (state.isSupportTouch && !(e as TouchEvent).targetTouches) {
    return false;
  }
  const et = e.targetTouches ? e.targetTouches[0] : e;
  const {
    sourceImgMouseDown: {on, mX, mY, x, y},
    scale,
  } = state;
  const sim = sourceImgMasking.value;
  const nX = et.screenX;
  const nY = et.screenY;
  const dX = nX - mX;
  const dY = nY - mY;
  let rX = x + dX;
  let rY = y + dY;
  if (!on) return;
  if (rX > 0) {
    rX = 0;
  }
  if (rY > 0) {
    rY = 0;
  }
  if (rX < sim.width - scale.width) {
    rX = sim.width - scale.width;
  }
  if (rY < sim.height - scale.height) {
    rY = sim.height - scale.height;
  }
  scale.x = rX;
  scale.y = rY;
}

function startRotateRight() {
  const {scale} = state;
  scale.rotateRight = true;
  const rotate = () => {
    if (scale.rotateRight) {
      const degree = ++scale.degree;
      createImg(degree);
      setTimeout(rotate, 60);
    }
  };
  rotate();
}

function startRotateLeft() {
  const {scale} = state;
  scale.rotateLeft = true;
  const rotate = () => {
    if (scale.rotateLeft) {
      const degree = --scale.degree;
      createImg(degree);
      setTimeout(rotate, 60);
    }
  };
  rotate();
}

function endRotate() {
  const {scale} = state;
  scale.rotateLeft = false;
  scale.rotateRight = false;
}

function startZoomAdd() {
  const {scale} = state;
  scale.zoomAddOn = true;
  const zoom = () => {
    if (scale.zoomAddOn) {
      const range = scale.range >= 100 ? 100 : ++scale.range;
      zoomImg(range);
      setTimeout(zoom, 60);
    }
  };
  zoom();
}

function endZoomAdd() {
  state.scale.zoomAddOn = false;
}

function startZoomSub() {
  const {scale} = state;
  scale.zoomSubOn = true;
  const zoom = () => {
    if (scale.zoomSubOn) {
      const range = scale.range <= 0 ? 0 : --scale.range;
      zoomImg(range);
      setTimeout(zoom, 60);
    }
  };
  zoom();
}

function endZoomSub() {
  state.scale.zoomSubOn = false;
}

function zoomChange(e: Event) {
  zoomImg((e.target as HTMLInputElement).valueAsNumber);
}

function zoomImg(newRange: number) {
  const {scale} = state;
  const {
    maxWidth,
    maxHeight,
    minWidth,
    minHeight,
    width,
    height,
    x,
    y
  } = scale;
  const sim = sourceImgMasking.value;
  const sWidth = sim.width;
  const sHeight = sim.height;
  const nWidth = minWidth + ((maxWidth - minWidth) * newRange) / 100;
  const nHeight = minHeight + ((maxHeight - minHeight) * newRange) / 100;
  let nX = sWidth / 2 - (nWidth / width) * (sWidth / 2 - x);
  let nY = sHeight / 2 - (nHeight / height) * (sHeight / 2 - y);
  if (nX > 0) {
    nX = 0;
  }
  if (nY > 0) {
    nY = 0;
  }
  if (nX < sWidth - nWidth) {
    nX = sWidth - nWidth;
  }
  if (nY < sHeight - nHeight) {
    nY = sHeight - nHeight;
  }
  scale.x = nX;
  scale.y = nY;
  scale.width = nWidth;
  scale.height = nHeight;
  scale.range = newRange;
  setTimeout(() => {
    if (scale.range === newRange) {
      createImg();
    }
  }, 300);
}

function createImg(e?: any) {
  const {
    mime,
    sourceImg,
    scale: {x, y, width, height, degree}
  } = state;
  const canvas = canvasRef.value!;
  const ctx = canvas.getContext('2d')!;
  if (e) {
    state.sourceImgMouseDown.on = false;
  }
  canvas.width = props.width;
  canvas.height = props.height;
  ctx.clearRect(0, 0, props.width, props.height);
  ctx.fillStyle = '#fff';
  ctx.fillRect(0, 0, props.width, props.height);
  ctx.translate(props.width * 0.5, props.height * 0.5);
  ctx.rotate((Math.PI * degree) / 180);
  ctx.translate(-props.width * 0.5, -props.height * 0.5);
  ctx.drawImage(
      sourceImg!,
      // 左移
      x / sourceImgMasking.value.scale - 80,
      y / sourceImgMasking.value.scale,
      width / sourceImgMasking.value.scale,
      height / sourceImgMasking.value.scale
  );
  state.createImgUrl = canvas.toDataURL(mime);
}

function prepareUpload() {
  const {url, createImgUrl, field, ki} = state;
  emit('crop-success', createImgUrl, field, ki);
  if (url) {
    upload()
  } else {
    off()
  }
}

function upload() {
  const {lang, mime, url, field, ki, createImgUrl} = state
  //将图片上传服务器中
  let formParams = new FormData();
  formParams.append("file", data2blob(createImgUrl, mime))
  formParams.append("token", getCookie(cookieToken) as string)
  formParams.append("platform", "web")
  formParams.append("source", "picture")
  formParams.append("userUid", "uid00000000000000000000000000000000")
  formParams.append("adminUid", "uid00000000000000000000000000000000")
  formParams.append("projectName", "blog")
  formParams.append("sortName", "admin")
  console.log("upload传入的参数", formParams)
  reset()
  state.loading = 1
  setStep(3)
  request({
    url,
    method: 'post',
    data: formParams
  }).then((response: any) => {
    state.loading = 2
    if (response.uploaded === 1) {
      console.log("上传成功", response)
      emit('crop-upload-success', response)
    } else {
      message.error(response.error.message)
      state.loading = 3
      state.hasError = true
      state.errorMsg = response.error.message
    }
  }).catch(err => {
    if (state.value) {
      state.loading = 3
      state.hasError = true
      state.errorMsg = lang.fail
      emit('crop-upload-fail', err, field, ki)
    }
  })
}

function closeHandler(e: any) {
  const {value} = state;
  if (value && (e.key === 'Escape' || e.keyCode === 27)) {
    off()
  }
}
</script>

<template>
  <div v-show="value" class="vue-image-crop-upload">
    <div class="vicp-wrap">
      <div class="vicp-close" @click="off">
        <i class="vicp-icon4"/>
      </div>

      <div v-show="state.step == 1" class="vicp-step1">
        <div
            class="vicp-drop-area"
            @dragleave="preventDefault"
            @dragover="preventDefault"
            @dragenter="preventDefault"
            @click="handleClick"
            @drop="handleChange"
        >
          <i v-show="state.loading != 1" class="vicp-icon1">
            <i class="vicp-icon1-arrow"/>
            <i class="vicp-icon1-body"/>
            <i class="vicp-icon1-bottom"/>
          </i>
          <span v-show="state.loading !== 1" class="vicp-hint">{{ state.lang.hint }}</span>
          <span v-show="!state.isSupported" class="vicp-no-supported-hint">{{ state.lang.noSupported }}</span>
          <input v-show="false" v-if="state.step == 1" ref="fileinput" type="file" @change="handleChange">
        </div>
        <div v-show="state.hasError" class="vicp-error">
          <i class="vicp-icon2"/>
          {{ state.errorMsg }}
        </div>
        <div class="vicp-operate">
          <a @click="off" @mousedown="ripple">{{ state.lang.btn.off }}</a>
        </div>
      </div>

      <div v-if="state.step == 2" class="vicp-step2">
        <div class="vicp-crop">
          <div v-show="true" class="vicp-crop-left">
            <div class="vicp-img-container">
              <img
                  ref="img"
                  :src="state.sourceImgUrl"
                  :style="sourceImgStyle"
                  class="vicp-img"
                  draggable="false"
                  @drag="preventDefault"
                  @dragstart="preventDefault"
                  @dragend="preventDefault"
                  @dragleave="preventDefault"
                  @dragover="preventDefault"
                  @dragenter="preventDefault"
                  @drop="preventDefault"
                  @touchstart="imgStartMove"
                  @touchmove="imgMove"
                  @touchend="createImg"
                  @touchcancel="createImg"
                  @mousedown="imgStartMove"
                  @mousemove="imgMove"
                  @mouseup="createImg"
                  @mouseout="createImg"
                  alt="ImgContainer">
              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-1"/>
              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-2"/>
            </div>

            <div class="vicp-range">
              <input
                  :value="state.scale.range"
                  type="range"
                  step="1"
                  min="0"
                  max="100"
                  @input="zoomChange"
              >
              <i
                  class="vicp-icon5"
                  @mousedown="startZoomSub"
                  @mouseout="endZoomSub"
                  @mouseup="endZoomSub"
              />
              <i
                  class="vicp-icon6"
                  @mousedown="startZoomAdd"
                  @mouseout="endZoomAdd"
                  @mouseup="endZoomAdd"
              />
            </div>

            <div v-if="!noRotate" class="vicp-rotate">
              <i @mousedown="startRotateLeft" @mouseout="endRotate" @mouseup="endRotate">↺</i>
              <i @mousedown="startRotateRight" @mouseout="endRotate" @mouseup="endRotate">↻</i>
            </div>
          </div>
          <div v-show="true" class="vicp-crop-right">
            <div class="vicp-preview">
              <div v-if="!noSquare" class="vicp-preview-item">
                <img :src="state.createImgUrl" :style="previewStyle" alt="">
                <span>{{ state.lang.preview }}</span>
              </div>
              <div v-if="!noCircle" class="vicp-preview-item vicp-preview-item-circle">
                <img :src="state.createImgUrl" :style="previewStyle" alt="">
                <span>{{ state.lang.preview }}</span>
              </div>
            </div>
          </div>
        </div>
        <div class="vicp-operate">
          <a @click="setStep(1)" @mousedown="ripple">{{ state.lang.btn.back }}</a>
          <a class="vicp-operate-btn" @click="prepareUpload" @mousedown="ripple">{{ state.lang.btn.save }}</a>
        </div>
      </div>

      <div v-if="state.step == 3" class="vicp-step3">
        <div class="vicp-upload">
          <span v-show="state.loading === 1" class="vicp-loading">{{ state.lang.loading }}</span>
          <div class="vicp-progress-wrap">
            <span v-show="state.loading === 1" :style="progressStyle" class="vicp-progress"/>
          </div>
          <div v-show="state.hasError" class="vicp-error">
            <i class="vicp-icon2"/>
            {{ state.errorMsg }}
          </div>
          <div v-show="state.loading === 2" class="vicp-success">
            <i class="vicp-icon3"/>
            {{ state.lang.success }}
          </div>
        </div>
        <div class="vicp-operate">
          <a @click="setStep(2)" @mousedown="ripple">{{ state.lang.btn.back }}</a>
          <a @click="off" @mousedown="ripple">{{ state.lang.btn.close }}</a>
        </div>
      </div>
      <canvas v-show="false" ref="canvasRef" :width="width" :height="height"/>
    </div>
  </div>
</template>

<style scoped lang="less">
@import "./style/cropper.css";
</style>
